feat: give the native inserter button platform-appropriate press feedback - #577
Merged
Conversation
Move the add block button rules out of the editor toolbar stylesheet and into a dedicated `native-inserter/style.scss`, matching the colocated stylesheet convention used by the other components. Pure move, no visual change. The selector remains scoped to the toolbar ancestor to preserve specificity over Gutenberg's button defaults. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The add block button had no pressed state, so a tap produced no visual confirmation that it registered. Touch devices have no hover state, making the pressed state the only available affordance. Darken the icon's background while pressed. The pressed state applies instantly and fades back on release, so the feedback does not read as laggy. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The add block button was styled solely for iOS, so its circular container looked out of place against Android's shape language. Give Android a rounded square that morphs to full-round while pressed, per Material 3's icon button. The fill, size, and press color stay shared with iOS so the button still balances with Gutenberg's toolbar; only the corner treatment forks. The fill and the shape need opposite press treatments: the fill darkens instantly so the tap does not read as laggy, while the morph stays animated in both directions, since the deformation is the expressive gesture Material specifies. Setting `transition-duration` alone would flatten both, so the pressed rules narrow `transition-property` and give each property its own duration and easing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The button had no iOS-specific press motion, and the shared rule snapped its fill instantly, which reads harsher than a native control. Grow the button while pressed and fade the fill in over 0.1s, easing both back over 0.2s. Small controls grow rather than shrink so their edges stay visible around the finger; the slower release is what reads as UIKit. Move motion out of the shared rule so each platform declares its own complete transition list. The Android pressed rule previously had to re-declare `transition-property` because the shared rule narrowed it, which left parallel duration and easing lists to keep index-aligned across platform blocks. Only static appearance is shared now. Scope the non-Android rules with `:not(.is-android)` rather than `is-ios`, since neither platform class is set when the editor runs in a browser during development. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The Android demo app opted out of the native inserter by default, so verifying it required flipping a toggle on every launch. The iOS demo app already enables it via `applyDemoAppDefaults`. Default the toggle on to match. The dependent "Inserter Media Strip" toggle becomes interactive by default as a result, since it is gated on the native inserter being enabled. Only the demo app changes; the `EditorConfiguration` default stays `false` on both platforms, so host apps are unaffected. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The button's Android motion used invented values: an easing curve ending at 1.2 that overshot the target radius, and a duration inherited from the iOS timing variable rather than chosen for Android. Replace both with the tokens from the `material3` artifact the Android library builds against — `EasingStandard`, `cubic-bezier(0.2, 0, 0, 1)`, and `DurationShort3`, 150ms. The standard easing settles flat rather than overshooting, and Short 3 is the token for a small component's state change. Apply them to the scale as well as the shape morph, since both are Android motion and had no reason to differ. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The button's iOS motion used invented values: an asymmetric 0.1s/0.2s pair with `ease-out`. SwiftUI animates a state change with a single spring rather than differing in and out timings, and none of its presets are eased durations. Sample `.snappy` — duration 0.5, bounce 0.15, so a damping ratio of 0.85 — across its settling time into a CSS `linear()` easing, and apply it in both directions. The overshoot lands well under a device pixel at this size, so what the approximation buys is the spring's rise shape, which `ease-out` does not reproduce. `linear()` needs WKWebView 17.2 while the package deploys to iOS 17.0, so an eased fallback precedes the `@supports` block. Without it, 17.0 and 17.1 would drop to constant-velocity easing, which reads worse than the curve being replaced. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
XCFramework BuildThis PR's XCFramework is available for testing. Add the following to your .package(url: "https://github.com/wordpress-mobile/GutenbergKit", branch: "pr-build/577")Built from 0e8d318 |
Enabling the native inserter by default in the demo app broke `testUndoRedoAfterTyping`. `insertBlock` tapped the "Add block" toggle and then waited for a web popover, but the toggle now dispatches over the bridge and presents the native Compose picker, so the web dialog never appeared and the wait timed out. Tap the native block tile instead. `BlockTile` is a clickable `Role.Button`, and `clickable` merges descendants, so the label resolves onto the tile itself in the merged tree and the matcher pairs `hasClickAction` with `hasText`. Category tabs are clickable and labelled too, so block names that collide with a tab name would be ambiguous — noted in a comment, since only "Paragraph" is used today. `insertBlock` and `typeInContent` now take the Compose rule, matching the other rule-based helpers. Drops the web inserter's dialog selector and XPath builder, which no longer have callers. Verified against the emulator: both tests in `EditorInteractionTest` pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
dcalhoun
force-pushed
the
feat/native-inserter-button-press-feedback
branch
from
August 2, 2026 00:46
1d1cea0 to
6b5bdcb
Compare
…ease The pressed rule set `transition-duration` positionally against the base rule's property list, so the `0s` only landed on `background-color` by matching index — reordering the base list would silently retarget it. The same coupling this file's structure was meant to remove. It was also one-directional: `0s` applied only while `:active` held, so on release the fill faded back over 150ms, contradicting Material's immediate state layer. Declare the full `transition` shorthand in both rules. The fill now has no transition in either direction, and neither rule depends on the other's property order. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
An unscoped `hasText` match would resolve two nodes for any block name that
is also a category tab ("Text", "Media", "Design"), since tabs are clickable
and labelled too. `waitForNode` wraps `assertExists` in `waitUntilAsserts`,
which swallows the exception — so the ambiguity would surface as a 30s
timeout rather than an "expected 1 node but found 2" failure.
Require a `CollectionInfo` ancestor, which the block grid is the only node
in the sheet to carry, making the collision structurally impossible instead
of merely undocumented.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The `preventDefault()` on mousedown looks like it would cancel the button's `:active` styles, which the press feedback depends on. It does not — both WebViews apply `:active` from the hit test on pointer down, independently of the default action being cancelled, verified on device. Note it at the call site so the interaction does not have to be re-investigated. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`waitForNode` polled on `assertExists`, which throws for both "no match yet" and "more than one match". `waitUntilAsserts` swallows either, so an ambiguous matcher burned the full timeout and reported "condition still not satisfied" — hiding the cause behind a 30s hang. Poll on the match count instead, then assert the count is exactly one. An ambiguous matcher now fails as soon as its nodes appear, naming the matcher and the number found. `waitForNodeWithText` routes through the same helper; each of its callers clicks the node afterwards, which already required a unique match. Verified by forcing an ambiguous matcher: fails in 12s with "found 26" rather than hanging for 30s. Also corrects the scoping comment. `hasText` compares exactly, so no current block title collides with a category tab; the grid scoping makes uniqueness a property of the matcher rather than of the current strings. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The pressed fill was the one value in this file with no provenance. Every other value cites its source — `DurationShort3` and `EasingStandard` for the Android motion, a sampled `.snappy` spring for iOS, a stated rationale for the shape and scale — but `#d5d3d9` was picked by eye, a flat 7.6% multiply of each channel from the resting fill. Derive it instead from Material 3's pressed state layer: `on-surface-variant` composited over the container. The spec's 10% resolves to #dad9dc, which under-reads at 32px, where the fill is largely occluded by the finger at the moment it changes. 16% is the top of the range that still reads as a state layer rather than a differently colored control, and resolves to #d0cfd3 — ΔL* 9.4 from the resting fill, up from 7.6. The same value serves iOS, where a UIKit highlight reads as a low-alpha black overlay and lands in the same range, so the fill stays shared as before. Use `color.mix` with an explicit `@use "sass:color"` rather than the global `mix()`, which is deprecated in Dart Sass and warns on build. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
At 32px the previous 1.08 moved each edge by 1.28px — 2.56 device px at 2x, barely above the threshold where the growth registers at all. The fill is largely occluded by the finger at the moment it changes, so the edges are what actually carries the press, and they were doing the least work. Raise the scale to 1.15, which grows each edge by 2.4px. The painted circle stays 9.2px inside the toolbar's 46px tap target, so neighboring controls cannot collide at this or any nearby value. Chosen from the geometry rather than from device observation; 1.20 is the next step if this still under-reads, beyond which a control this small starts to read as springy rather than responsive. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
dcalhoun
marked this pull request as ready for review
August 3, 2026 14:51
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What?
Gives the native inserter button press feedback, and replaces its iOS-only styling on Android with Material 3's shape and motion.
Why?
Tapping the button produced no visual confirmation that it registered. Touch devices have no hover state, so the pressed state is the only affordance available.
The button's circular container and its motion were authored for iOS and applied unconditionally, so on Android the shape read as out of place against the platform's shape language.
How?
Split into reviewable commits:
editor-toolbar/style.scssinto a newnative-inserter/style.scss. Pure move, no visual change.EasingStandardandDurationShort3.linear()easing sampled from SwiftUI's.snappyspring. Small controls grow rather than shrink so their edges stay visible around the finger.EditorConfigurationdefault staysfalse, so host apps are unaffected.Only static appearance is shared between platforms; each declares its own complete transition list. Non-Android rules are scoped
:not(.is-android)rather thanis-ios, since neither class is set in a browser during development.linear()requires WKWebView 17.2 whilePackage.swiftdeploys to iOS 17.0, so an eased fallback precedes the@supportsblock.Testing Instructions
+) button in the editor toolbar.Accessibility Testing Instructions
No change to the button's semantics, accessible name, or focus behavior — these styles are purely visual and apply on
:active. The existing "Add block" label and 46px minimum touch target are unchanged.Screenshots or screencast
android.mp4
ios.mov